package server;

import help.AdventureHelp;
import help.Help;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import player.PlayerManager;
import player.PlayerManagerImp;
import server.util.Broadcaster;
import server.util.BroadcasterImp;
import server.util.Ticker;
import server.util.TickerImp;
import server.util.tickevents.*;
import adventure.Adventure;
import adventure.AdventureEdit;
import adventure.AdventureInput;
import adventure.AdventureInputImp;
import channel.master.MasterChannelTest;
import colour.Colour;
import colour.ColourUtil;
import console.IOBuffer;
import java.io.File;
import java.util.StringTokenizer;
import parser.AdventureParse;
import server.util.ServerConstants;

/**
 *
 * @author Michael Hanns
 *
 */
public class NetworkServerImp extends Thread implements NetworkServer {

    private double version = 1.1;
    private String serverName;
    private int serverPort;
    private int maxCapacity;
    private boolean hostingServer;
    private ServerSocket server;
    private IOBuffer terminal;
    private NetworkPreferences netprefs;
    private ExecutorService exec;
    private ArrayList<NetworkClientThread> playerThreads;
    private PlayerManager players;
    private Adventure adv;
    private AdventureInput advInp;
    private Broadcaster broadcaster;
    private Ticker serverTicker;
    private Help advHelp;
    private Help shellHelp;
    private ArrayList<String> bannedIPs;

    public NetworkServerImp(IOBuffer term, NetworkPreferences prefs, Help shHelp, double vers) {
        terminal = term;
        hostingServer = false;
        netprefs = prefs;
        shellHelp = shHelp;
        playerThreads = new ArrayList<NetworkClientThread>();
        bannedIPs = new ArrayList<String>();
        broadcaster = new BroadcasterImp();
        this.version = vers;
    }

    @Override
    public void run() {
        acceptConnections();
    }

    private void acceptConnections() {
        try {
            while (hostingServer) {
                removeIdleThreads();
                Socket newCon = server.accept();
                boolean banned = false;
                System.out.println("Connection incoming: " + newCon.getInetAddress().getHostAddress());
                for (int x = 0; x < bannedIPs.size() && !banned; x++) {
                    if (newCon.getInetAddress().getHostAddress().equals(bannedIPs.get(x))) {
                        banned = true;
                    }
                }

                if (banned) {
                    PrintWriter out = new PrintWriter(newCon.getOutputStream(), true);
                    out.println("Cannot connect: your IP address has been banned.");
                    newCon.close();
                    newCon = null;
                } else if (players.onlinePlayers() >= maxCapacity) {
                    PrintWriter out = new PrintWriter(newCon.getOutputStream(), true);
                    out.println("This server is currently full at the moment. Please try again later.\r");
                    out.println("You are now disconnected. Thanks for playing "
                            + adv.getName() + ".");
                    newCon.close();
                    newCon = null;
                } else {
                    final NetworkClientThread newGame = new NetworkClientThreadImp(this, newCon, advInp,
                            players, broadcaster, netprefs.getMOTD(), getGreeting(), maxCapacity, version);
                    Runnable task = new Runnable() {
                        @Override
                        public void run() {
                            newGame.start();
                        }
                    };

                    playerThreads.add(newGame);
                    exec.submit(task);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String getGreeting() {
        if (adv.getLoginScreen().length() > 0) {
            return adv.getLoginScreen() + "\r";
        } else {
            return "WITNA Server v" + version + ", you are playing " + adv.getName()
                    + " v" + adv.getVersion() + ".\r\n\nWhat is thy name, adventurer?\r";
        }
    }

    private void removeIdleThreads() {
        for (int x = 0; x < playerThreads.size(); x++) {
            if (playerThreads.get(x).disconnected()) {
                playerThreads.get(x).disconnect(false);
                playerThreads.remove(x);
                x--;
            }
        }
    }

    // For the local client users commands
    @Override
    public void processLocalCommand(String input) {
        if (input.equalsIgnoreCase("DISCONNECT") || input.equalsIgnoreCase("QUIT")) {
            disconnect();
        } else if (input.toUpperCase().startsWith("MOTD")) {
            if (input.length() == 4) {
                terminal.writeTo("\nSet the Message of the Day to what?");
            } else if (input.length() == 5 && input.charAt(4) == ' ') {
                terminal.writeTo("\nSet the Message of the Day to what?");
            } else if (input.length() > 5 && input.charAt(4) == ' ') {
                setMOTD(ColourUtil.colourise(input.substring(5), ColourUtil.yellow)
                        + ColourUtil.colourise("", ColourUtil.grey));
            }
        } else if (input.toUpperCase().startsWith("SERVERMSG")) {
            if (input.length() == 9) {
                terminal.writeTo("\nYou need to enter a server message to broadcast.");
            } else if (input.length() == 10 && input.charAt(9) == ' ') {
                terminal.writeTo("\nYou need to enter a server message to broadcast.");
            } else if (input.length() > 10 && input.charAt(9) == ' ') {
                broadcaster.broadcastServerMessage(input.substring(10));
                terminal.writeTo("\nSERVERMESSAGE sent: '"
                        + ColourUtil.colourise(input.substring(10), ColourUtil.white)
                        + ColourUtil.colourise("'", ColourUtil.grey));
            }
        } else if (input.toUpperCase().startsWith("CHANNELJOIN")) {
            if (input.length() == 11) {
                terminal.writeTo("\nChannel not specified.\nSyntax: channeljoin <channelipaddress>");
            } else if (input.length() == 12 && input.charAt(11) == ' ') {
                terminal.writeTo("\nChannel not specified.\nSyntax: channeljoin <channelipaddress>");
            } else if (input.length() > 12 && input.charAt(11) == ' ') {
                String channelIP = input.substring(12).trim();
                setChannel(channelIP);
            }
        } else if (input.toUpperCase().startsWith("SERVERJOIN")) {
            if (input.length() == 11 || (input.length() == 12 && input.charAt(11) == ' ')
                    || (input.length() > 12 && input.charAt(11) == ' ')) {
                terminal.writeTo("\nYou are currently hosting a server in this terminal. "
                        + "If you wish to join a server, please open a new instance of WITNA "
                        + "or disconnect the server first.");
            }
        } else if (input.equalsIgnoreCase("SAVE")) {
            if (players.savePlayers()) {
                terminal.writeTo("\n" + ColourUtil.colourise("Player status saved.", ColourUtil.green)
                        + ColourUtil.colourise("", ColourUtil.grey));
            } else {
                terminal.writeTo("\nERROR: Player status not saved. Cannot create file. Ensure WITNA is not installed to a read-only directory.");
            }
        } else if (input.equalsIgnoreCase("PLAYERS")) {
            terminal.writeTo("\nConnected players:");
            if (playerThreads.size() > 0) {
                String name;
                for (int x = 0; x < playerThreads.size(); x++) {
                    name = playerThreads.get(x).onlinePlayer();
                    if (name.length() > 0) {
                        terminal.writeTo("\n" + ColourUtil.colourise(name, ColourUtil.player));
                    }
                }
            } else {
                terminal.writeTo("\nThere are currently no connected players.");
            }
        } else if (input.toUpperCase().startsWith("HELP")) {
            if (input.length() == 4) {
                help("");
            } else if (input.length() == 5 && input.charAt(4) == ' ') {
                help("");
            } else if (input.length() > 5 && input.charAt(4) == ' ') {
                help(input.substring(5));
            }
        } else if (input.startsWith("?")) {
            if (input.length() == 1) {
                help("");
            } else if (input.length() == 2 && input.charAt(1) == ' ') {
                help("");
            } else if (input.length() > 2 && input.charAt(1) == ' ') {
                help(input.substring(2));
            }
        } else if (input.toUpperCase().startsWith("KICK")) {
            String[] tokens = input.split(" ");
            if (tokens.length > 2) {
                terminal.writeTo("\n" + kick(tokens[1], input.substring(tokens[0].length() + tokens[1].length() + 2)).replace("\n", "\r\n"));
            } else if (tokens.length > 1) {
                terminal.writeTo("\n" + kick(tokens[1], "").replace("\n", "\r\n"));
            } else {
                terminal.writeTo("\nKick whom [for what reason]?\r\nSyntax: kick <playername> [<reason>]");
            }
        } else if (input.toUpperCase().startsWith("BAN")) {
            String[] tokens = input.split(" ");
            if (tokens.length > 2) {
                terminal.writeTo("\n" + ban(tokens[1], input.substring(tokens[0].length() + tokens[1].length() + 2)));
            } else if (tokens.length > 1) {
                terminal.writeTo("\n" + ban(tokens[1], ""));
            } else {
                terminal.writeTo("\nBan whom [for what reason]?\r\nSyntax: ban <playername> [<reason>]");
                terminal.writeTo("\nBanned IP's:");
                if (bannedIPs.size() > 0) {
                    for (int x = 0; x < bannedIPs.size(); x++) {
                        terminal.writeTo("\n" + bannedIPs.get(x));
                    }
                } else {
                    terminal.writeTo("\nThere are currently no IPs banned by this server.");
                }
            }
        } else {
            terminal.writeTo("\nServer command not recognised.");
        }
    }

    @Override
    public boolean hostServer(String name, String advPath, int capacity, int port) {
        // Start the server
        try {
            serverName = name;
            maxCapacity = capacity;
            serverPort = port;
            server = new ServerSocket(serverPort);

            exec = Executors.newFixedThreadPool(maxCapacity + 1); // +1 for ticker
            File file = new File(advPath);
            AdventureParse p = new AdventureParse();
            p.readAdventureFileForServer(file, broadcaster);
            adv = p.getAdventure();
        } catch (IOException e) {
            return false;
        }

        if (adv != null && ((AdventureEdit) adv).getSoftwareVersion() > version) {
            terminal.writeTo("File opening unsuccessful - adventure was created in a more recent version of WITNA. Please obtain a more up-to-date version of WITNA before trying again.");
            return false;
        } else if (adv != null) {

            // Set up all the server side tools to work with adv
            ((AdventureEdit) adv).setBroadcaster(broadcaster);
            players = new PlayerManagerImp(netprefs.getPlayerFilePath() + "/" + adv.getName() + "/" + name + "/", adv);
            System.out.println("Players == null? ServerHost" + (players == null));
            ((AdventureEdit) adv).setPlayerManager(players);

            advInp = new AdventureInputImp(adv);

            setupTicker();

            if (((AdventureEdit) adv).ready()) {

                terminal.writeTo("\nAdventure file read, starting server...\n");

                // Print readout
                terminal.writeTo(ColourUtil.colourise("\nWITNA Server v" + version, ColourUtil.green));
                terminal.writeTo(ColourUtil.colourise("\n-----", ColourUtil.white));
                terminal.writeTo(ColourUtil.colourise("\n-", ColourUtil.white)
                        + ColourUtil.colourise("Server      : ", ColourUtil.green)
                        + ColourUtil.colourise(serverName + "/" + adv.getName()
                        + " v" + adv.getVersion(), ColourUtil.yellow));
                terminal.writeTo(ColourUtil.colourise("\n-", ColourUtil.white)
                        + ColourUtil.colourise("Global ip   : ", ColourUtil.green));

                final int serverPort = port;
                Runnable task = new Runnable() {
                    @Override
                    public void run() {
                        // Try and get internet IP
                        String globalip = getGlobalIP();

                        // Try and get network IP
                        String localip = getLocalIP();

                        // Finish output
                        terminal.writeTo(ColourUtil.colourise(globalip + ":" + serverPort, ColourUtil.yellow));
                        terminal.writeTo(ColourUtil.colourise("\n-", ColourUtil.white)
                                + ColourUtil.colourise("Local  ip   : ", ColourUtil.green)
                                + ColourUtil.colourise(localip + ":" + serverPort, ColourUtil.yellow));
                        terminal.writeTo(ColourUtil.colourise("\n-", ColourUtil.white)
                                + ColourUtil.colourise("Max capacity: ", ColourUtil.green)
                                + ColourUtil.colourise(maxCapacity + "", ColourUtil.yellow));
                        String status = getNetStatus(globalip, localip);
                        terminal.writeTo(ColourUtil.colourise("\n-", ColourUtil.white)
                                + ColourUtil.colourise("Connection  : ", ColourUtil.green)
                                + ColourUtil.colourise(status, ColourUtil.yellow)
                                + ColourUtil.colourise("\n", ColourUtil.grey));

                        advHelp = new AdventureHelp(terminal);
                        ((AdventureEdit) adv).setHelpFile(advHelp);
                    }
                };
                new Thread(task).start();


                // If all this works, you're hosting
                hostingServer = true;

                return true;
            } else {
                terminal.writeTo("\nInternal error: PlayerManager or Broadcaster not yet set!");
                return true;
            }
        } else {
            terminal.writeTo("\nAdventure file not found. Cancelling serverhost..");
            return false;
        }
    }

    private void setupTicker() {
        Runnable ticker = new Runnable() {
            @Override
            public void run() {
                serverTicker = new TickerImp(adv, broadcaster, players);
                // Add cleanup/destroy empty corpses tick event
                serverTicker.addTickEvent(new CleanupRooms(broadcaster,
                        ((AdventureEdit) adv).getRoomManager()));
                // Add combat round event
                serverTicker.addRoundEvent(new ExecuteCombat(adv));
                // Add repop tick event
                serverTicker.addTickEvent(new Repopulate(broadcaster,
                        ((AdventureEdit) adv).getRoomManager(), players));
                // Add regen tick event
                serverTicker.addTickEvent(new RegeneratePlayers(players));
                // Add save players tick event
                serverTicker.addTickEvent(new SavePlayers(players));
                serverTicker.startTicking();
            }
        };
        exec.submit(ticker);
    }

    @Override
    public void disconnect() {
        terminal.writeTo("\nDisconnecting server...");

        terminal.writeTo("\nPlayer status saved.");

        try {
            if (hostingServer) {
                serverTicker.stopTicking();
                for (int x = 0; x < playerThreads.size(); x++) {
                    playerThreads.get(x).disconnect(true);
                }
                server.close();
            }
            serverTicker = null;
            exec = null;
            server = null;
            hostingServer = false;
        } catch (IOException e) {
            terminal.writeTo("\nError disconnecting server!");
        }
    }

    private void setMOTD(String motd) {
        netprefs.setMOTD(motd);
        terminal.writeTo("\nMessage of the Day set to: " + motd);
    }

    private void setChannel(String channelIP) {
        final String chanDets = serverPort + " `" + serverName + "` " + "`" + adv.getName() + "` "
                + "`" + adv.getDescription() + "` " + "`" + adv.getVersion() + "` "
                + "`" + maxCapacity + "`";


        if (channelIP.equalsIgnoreCase("DEFAULT")) {
            terminal.writeTo("\nFetching default channel IP..");
            channelIP = MasterChannelTest.getDefaultChannelIP();
        }
        final String finalIP = channelIP;
        terminal.writeTo("\nCommunicating with channel..");
        Runnable task = new Runnable() {
            @Override
            public void run() {
                terminal.writeTo(serverTicker.setChannel(finalIP, chanDets));
            }
        };

        exec.submit(task);


    }

    private String getGlobalIP() {
        try {
            URL whatismyip = new URL(ServerConstants.externalIPAddr);
            InputStream reader = whatismyip.openStream();
            BufferedReader in = new BufferedReader(new InputStreamReader(reader));

            String ip = in.readLine();
            reader.close();
            in.close();

            if (ip != null) {
                return ip;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "127.0.0.1";


    }

    private String getLocalIP() {
        try {
            InetAddress addr = InetAddress.getLocalHost();
            return addr.getHostAddress();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "127.0.0.1";


    }

    private String getNetStatus(String globalip, String localip) {
        if (!globalip.equals("127.0.0.1")) {
            return "ONLINE";
        } else {
            if (!localip.equals("127.0.0.1")) {
                return "LOCAL";
            } else {
                return "OFFLINE";
            }
        }
    }

    private void help(String query) {
        String[] tokens = query.split(" ");
        if (tokens.length > 0) {
           String result = ColourUtil.colouriseAlternate("WITNA Client Shell v" + version + " Helpfiles", new Colour[] {ColourUtil.yellow, ColourUtil.darkYellow})
                    + ColourUtil.colouriseAlternate("\n----------------------------------", new Colour[] {ColourUtil.darkYellow, ColourUtil.yellow})
                    + shellHelp.query(query);

            terminal.writeTo("\n" + result);
        } else {
            String genericHelp =
                    ColourUtil.colouriseAlternate("WITNA Client Shell v" + version + " Helpfiles", new Colour[] {ColourUtil.yellow, ColourUtil.darkYellow})
                    + ColourUtil.colouriseAlternate("\n----------------------------------\n\n", new Colour[] {ColourUtil.darkYellow, ColourUtil.yellow})
                    + "Welcome to the WITNA Client Helpfiles. Here you will find "
                    + "a succinct collection of help text detailling the process of launching "
                    + "and maintaining of a WITNA "
                    + "custom server, along with basic "
                    + "getting started information for joining servers, etc.\n\n"
                    + "If you are a new user, try searching for '"
                    + ColourUtil.colourise("getting started", ColourUtil.white)
                    + "' "
                    + "or check the "
                    + ColourUtil.colouriseAlternate("WITNA", new Colour[] {ColourUtil.yellow, ColourUtil.darkYellow})
                    + ColourUtil.colourise(" Wiki ", ColourUtil.white)
                    + "at "
                    + ColourUtil.colourise("http://help.whatisthynameadventurer.com ", ColourUtil.white)
                    + "for more information.\n\n"
                    + "Syntax: help getting started";
            terminal.writeTo("\n" + genericHelp);
        }
    }

    // Admin functions
    @Override
    public String kick(String playerName, String reason) {
        String fullName;
        for (int x = 0; x < playerThreads.size(); x++) {
            fullName = playerThreads.get(x).onlinePlayer();

            if (playerName.equalsIgnoreCase(fullName)) {
                String kickMessage = "You have been kicked from " + serverName + "!";
                if (reason.length() > 0) {
                    kickMessage += " Reason: " + reason;
                }
                broadcaster.broadcastToPlayer(players.getOnlinePlayer(fullName),
                        kickMessage + "\r");
                playerThreads.get(x).disconnect(true);
                playerThreads.remove(x);
                return "You have successfully kicked " + ColourUtil.colourise(fullName, ColourUtil.player)
                        + "!";
            }
        }
        return "No-one by that name is playing!";
    }

    @Override
    public String ban(String playerName, String reason) {
        String fullName;
        for (int x = 0; x < playerThreads.size(); x++) {
            fullName = playerThreads.get(x).onlinePlayer();

            if (playerName.equalsIgnoreCase(fullName)) {
                String kickMessage = "You have been banned from " + serverName + "!";
                if (reason.length() > 0) {
                    kickMessage += " Reason: " + reason;
                }
                bannedIPs.add(playerThreads.get(x).getIP());
                broadcaster.broadcastToPlayer(players.getOnlinePlayer(fullName),
                        kickMessage + "\r");
                playerThreads.get(x).disconnect(true);
                playerThreads.remove(x);
                return "You have successfully banned " + ColourUtil.colourise(fullName, ColourUtil.player)
                        + "!";
            }
        }
        return "No-one by that name is playing!";
    }
}
